Data

The Madison Open Data portal has property tax information by parcel: https://data-cityofmadison.opendata.arcgis.com/datasets/tax-parcels-assessor-property-information/data

Data was manually downloaded as a shapefile and then saved as an rds object for faster loading.

library(tidyverse)
## -- Attaching packages --------------------------------------------------------- tidyverse 1.3.0 --
## v ggplot2 3.3.2     v purrr   0.3.4
## v tibble  3.0.3     v dplyr   1.0.2
## v tidyr   1.1.2     v stringr 1.4.0
## v readr   1.3.1     v forcats 0.5.0
## -- Conflicts ------------------------------------------------------------ tidyverse_conflicts() --
## x dplyr::filter() masks stats::filter()
## x dplyr::lag()    masks stats::lag()
library(sf)
## Linking to GEOS 3.8.0, GDAL 3.0.4, PROJ 6.3.1
library(ggplot2)

The dataset is large, with over 80,000 observations and 144 variables. It does not contain information on ownership.

parcels <- readRDS("data/parcels.rds")
str(parcels)
## tibble [80,158 x 144] (S3: sf/tbl_df/tbl/data.frame)
##  $ OBJECTID  : int [1:80158] 1 2 3 4 5 6 7 8 9 10 ...
##  $ Parcel    : chr [1:80158] "060801101019" "060801101027" "060801101035" "060801101043" ...
##  $ XRefParcel: chr [1:80158] "060801101019" "060801101027" "060801101035" "060801101043" ...
##  $ Address   : chr [1:80158] "2001 Rae Ln" "2005 Rae Ln" "2009 Rae Ln" "2013 Rae Ln" ...
##  $ DateParcel: Date[1:80158], format: "2020-06-03" "2020-06-03" ...
##  $ PropertyCl: chr [1:80158] "Residential" "Residential" "Residential" "Residential" ...
##  $ PropertyUs: chr [1:80158] "Single family" "Single family" "Single family" "Single family" ...
##  $ Assessment: int [1:80158] 1 1 1 1 1 1 1 1 1 1 ...
##  $ AreaName  : chr [1:80158] "Meadowood" "Meadowood" "Meadowood" "Meadowood" ...
##  $ MoreThanOn: chr [1:80158] NA NA NA NA ...
##  $ HomeStyle : chr [1:80158] "Ranch" "Ranch" "Ranch" "Ranch" ...
##  $ YearBuilt : int [1:80158] 1960 1959 1959 1962 1959 1962 1964 1965 1958 1959 ...
##  $ Bedrooms  : int [1:80158] 3 4 3 3 3 5 5 4 3 4 ...
##  $ FullBaths : int [1:80158] 1 1 1 2 2 2 2 2 1 2 ...
##  $ HalfBaths : int [1:80158] 2 1 1 1 0 0 0 0 1 0 ...
##  $ TotalLivin: int [1:80158] 1371 1488 1290 1043 1386 1008 990 1076 1208 1603 ...
##  $ FirstFloor: int [1:80158] 1371 1488 1290 1043 1386 1008 990 1076 1208 1603 ...
##  $ SecondFloo: int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ ThirdFloor: int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ AboveThird: int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ FinishedAt: int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ Basement  : int [1:80158] 1254 1008 1066 1008 1386 1008 935 1040 1208 1120 ...
##  $ FinishedBa: int [1:80158] 686 350 564 607 550 750 637 860 380 560 ...
##  $ ExteriorWa: chr [1:80158] "Wood" "Wood" "Aluminum/Vinyl" "Wood" ...
##  $ Exterior_1: chr [1:80158] NA NA NA NA ...
##  $ Fireplaces: int [1:80158] 1 1 0 0 1 1 1 0 0 1 ...
##  $ CentralAir: chr [1:80158] "YES" "YES" "YES" "YES" ...
##  $ PartialAss: chr [1:80158] NA NA NA NA ...
##  $ AssessedBy: chr [1:80158] NA NA NA NA ...
##  $ CurrentLan: int [1:80158] 70000 74800 79100 76000 71100 64700 73900 66000 74000 70600 ...
##  $ CurrentImp: int [1:80158] 157400 152700 145500 170100 167400 148100 183600 180100 151500 165600 ...
##  $ CurrentTot: int [1:80158] 227400 227500 224600 246100 238500 212800 257500 246100 225500 236200 ...
##  $ PreviousLa: int [1:80158] 66000 70600 74600 71700 67100 61000 69700 62300 69800 66600 ...
##  $ PreviousIm: int [1:80158] 148500 144000 137300 160500 157900 139800 173200 169900 142900 156200 ...
##  $ PreviousTo: int [1:80158] 214500 214600 211900 232200 225000 200800 242900 232200 212700 222800 ...
##  $ NetTaxes  : num [1:80158] 4541 4543 4482 4940 4778 ...
##  $ SpecialAss: num [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ OtherCharg: num [1:80158] 0 0 678 0 0 ...
##  $ TotalTaxes: num [1:80158] 4541 4543 5160 4940 4778 ...
##  $ LotSize   : num [1:80158] 14270 14718 18867 14984 13334 ...
##  $ Zoning1   : chr [1:80158] "SR-C1" "SR-C1" "SR-C1" "SR-C1" ...
##  $ Zoning2   : chr [1:80158] NA NA NA NA ...
##  $ Zoning3   : chr [1:80158] NA NA NA NA ...
##  $ Zoning4   : chr [1:80158] NA NA NA NA ...
##  $ FrontageSt: chr [1:80158] "Rae Ln" "Rae Ln" "Rae Ln" "Rae Ln" ...
##  $ WaterFront: chr [1:80158] "NO" "NO" "NO" "NO" ...
##  $ TIFDistric: int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ TaxSchoolD: chr [1:80158] NA NA NA NA ...
##  $ Attendance: chr [1:80158] "Madison" "Madison" "Madison" "Madison" ...
##  $ Elementary: chr [1:80158] "Huegel" "Huegel" "Huegel" "Huegel" ...
##  $ MiddleScho: chr [1:80158] "Toki" "Toki" "Toki" "Toki" ...
##  $ HighSchool: chr [1:80158] "Memorial" "Memorial" "Memorial" "Memorial" ...
##  $ Ward      : int [1:80158] 95 95 95 95 95 95 95 95 95 95 ...
##  $ StateAssem: int [1:80158] 78 78 78 78 78 78 78 78 78 78 ...
##  $ RefuseDist: chr [1:80158] "04B" "04B" "04B" "04B" ...
##  $ RefuseURL : chr [1:80158] "http://www.cityofmadison.com/streets/documents/tueB.pdf" "http://www.cityofmadison.com/streets/documents/tueB.pdf" "http://www.cityofmadison.com/streets/documents/tueB.pdf" "http://www.cityofmadison.com/streets/documents/tueB.pdf" ...
##  $ Previous_1: int [1:80158] 61700 66000 69700 67000 62700 57000 65100 58200 65200 62200 ...
##  $ Previous_2: int [1:80158] 145500 134600 128300 150000 150600 130700 161900 158800 133600 146000 ...
##  $ Previous_3: int [1:80158] 207200 200600 198000 217000 213300 187700 227000 217000 198800 208200 ...
##  $ AlderDistr: int [1:80158] 20 20 20 20 20 20 20 20 20 20 ...
##  $ Assessme_1: Date[1:80158], format: "2020-06-03" "2020-06-03" ...
##  $ BlockNumbe: int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ BuildingDi: int [1:80158] 2 2 2 2 2 2 2 2 2 2 ...
##  $ CapitolFir: chr [1:80158] "-" "-" "-" "-" ...
##  $ Conditiona: int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ CouncilHol: int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ DateAdded : Date[1:80158], format: "1970-01-01" "1970-01-01" ...
##  $ DeedPage  : int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ DeedRestri: int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ DeedVolume: int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ Electrical: int [1:80158] 1 1 1 1 1 1 1 1 1 1 ...
##  $ EnvHealthD: int [1:80158] 31 31 31 31 31 31 31 31 31 31 ...
##  $ ExemptionT: chr [1:80158] "-" "-" "-" "-" ...
##  $ FireDistri: int [1:80158] 1 1 1 1 1 1 1 1 1 1 ...
##  $ FloodPlain: int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ FuelStorag: int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ HeatingDis: int [1:80158] 2 2 2 2 2 2 2 2 2 2 ...
##  $ Holds     : chr [1:80158] NA NA NA NA ...
##  $ IllegalLan: int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ LandfillPr: int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ LandfillRe: chr [1:80158] NA NA NA NA ...
##  $ Landmark  : chr [1:80158] NA NA NA NA ...
##  $ LandscapeB: int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ LocalHisto: chr [1:80158] "-" "-" "-" "-" ...
##  $ LotDepth  : num [1:80158] 0 0 0 0 0 0 0 0 150 0 ...
##  $ LotNumber : int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ LotteryCre: int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ LotType1  : chr [1:80158] "1 - Regular" "1 - Regular" "2 - Irregular" "2 - Irregular" ...
##  $ LotType2  : chr [1:80158] "1 - Corner" "0 - No Exception" "0 - No Exception" "0 - No Exception" ...
##  $ LotWidth  : num [1:80158] 0 0 0 0 0 0 0 0 93 0 ...
##  $ MCDCode   : chr [1:80158] "MADC" "MADC" "MADC" "MADC" ...
##  $ NationalHi: int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ Neighborho: chr [1:80158] "No description entered" "No description entered" "No description entered" "No description entered" ...
##  $ Neighbor_1: chr [1:80158] "0 - No description entered" "0 - No description entered" "0 - No description entered" "0 - No description entered" ...
##  $ Neighbor_2: chr [1:80158] "0 - No description entered" "0 - No description entered" "0 - No description entered" "0 - No description entered" ...
##  $ Neighbor_3: chr [1:80158] "0 -" "0 -" "0 -" "0 -" ...
##  $ NoiseAirpo: int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ NoiseRailr: int [1:80158] 0 0 0 0 0 0 0 0 0 0 ...
##  $ NoiseStree: int [1:80158] 61 61 61 61 61 0 0 0 0 0 ...
##   [list output truncated]
##  - attr(*, "sf_column")= chr "geometry"
##  - attr(*, "agr")= Factor w/ 3 levels "constant","aggregate",..: NA NA NA NA NA NA NA NA NA NA ...
##   ..- attr(*, "names")= chr [1:143] "OBJECTID" "Parcel" "XRefParcel" "Address" ...

I don’t see a data dictionary, but most of the variable names are self-explanatory. Some interesting stuff: - There are variables for noise from streets, railroads, and the airport. But it’s unclear what the values mean. I initially thought they were decibels from noise measurements, but the values are other 0, or 60 (airport), 61 (street, railroad)

parcels %>% 
  filter(NoiseRailr > 0) %>% 
  select(Address, NoiseRailr) %>% 
  arrange(desc(NoiseRailr))
## Simple feature collection with 2121 features and 2 fields
## geometry type:  POLYGON
## dimension:      XY
## bbox:           xmin: -89.49105 ymin: 43.03158 xmax: -89.26573 ymax: 43.1533
## geographic CRS: WGS 84
## # A tibble: 2,121 x 3
##    Address        NoiseRailr                                            geometry
##    <chr>               <int>                                       <POLYGON [°]>
##  1 5905 Old Midd~         61 ((-89.48142 43.08084, -89.48215 43.08132, -89.4817~
##  2 5911 Old Midd~         61 ((-89.48253 43.08156, -89.4824 43.08161, -89.48191~
##  3 5917 Old Midd~         61 ((-89.48253 43.08156, -89.48283 43.08175, -89.4827~
##  4 1509 Capital ~         61 ((-89.48282 43.08222, -89.48275 43.08222, -89.4826~
##  5 1505 Capital ~         61 ((-89.48283 43.08175, -89.48365 43.08227, -89.4830~
##  6 1500 Capital ~         61 ((-89.48418 43.08262, -89.48371 43.08279, -89.4831~
##  7 1504 Capital ~         61 ((-89.48302 43.08305, -89.48302 43.08305, -89.4828~
##  8 9 Veblen Pl            61 ((-89.48302 43.08305, -89.48371 43.08279, -89.4838~
##  9 13 Veblen Pl           61 ((-89.48418 43.08262, -89.48473 43.08297, -89.4841~
## 10 17 Veblen Pl           61 ((-89.48503 43.08317, -89.4842 43.08342, -89.48415~
## # ... with 2,111 more rows
unique(parcels$HomeStyle)
##  [1] "Ranch"                          "Bi-level"                      
##  [3] "Modern two story"               "Colonial"                      
##  [5] "Townhouse"                      NA                              
##  [7] "Split-level"                    "Contemporary"                  
##  [9] "Cape Cod"                       "Townhouse Duplex"              
## [11] "Tudor"                          "Apartment"                     
## [13] "Garden"                         "Ranch Duplex"                  
## [15] "Townhouse End Unit"             "Old Style"                     
## [17] "Cottage"                        "Garden with loft"              
## [19] "Bungalow"                       "Flat"                          
## [21] "Victorian georgian regency"     "Spanish mediterranean"         
## [23] "New style modern international" "Mansion"                       
## [25] "Ranch End Unit"                 "Converted"                     
## [27] "Hi-rise"                        "Other"                         
## [29] "Rowhouse"

Property tax per sqft

Add a new variable for NetTaxes per sq ft.

parcels2 <- parcels %>% 
  mutate(tax_per_sqft = NetTaxes / LotSize)
parcels2 %>% 
  mutate(Zoning1 = fct_reorder(Zoning1, tax_per_sqft, median)) %>% 
  group_by(Zoning1) %>% 
  ggplot(aes(Zoning1, tax_per_sqft)) +
  geom_boxplot() +
  scale_y_log10() +
  coord_flip()
## Warning: Transformation introduced infinite values in continuous y-axis
## Warning: Removed 21291 rows containing non-finite values (stat_boxplot).

Unfortunately, the Zoning1 field doesn’t always contain the current zoning code. For instance, there are many codes starting with HIS-. I firstx thought these may represent hstorical zoning codes, but it looks like they are the city’s designated historic districts. https://www.cityofmadison.com/dpced/planning/local-historic-districts/1601/ In most cases the actual zoning district then is in the Zoning2 field, and in a few cases, you have to go to Zoning3.

parcels3 <- parcels2 %>% 
  mutate(zoning_clean = case_when(str_detect(Zoning1, "HIS-") & !str_detect(Zoning2, "HIS-") ~ Zoning2,
                                  str_detect(Zoning1, "HIS-") & str_detect(Zoning2, "HIS-") ~ Zoning3,
                                  TRUE ~ Zoning1))
library(mapdeck)
## 
## Attaching package: 'mapdeck'
## The following object is masked from 'package:tibble':
## 
##     add_column
mapdeck(style = 'mapbox://styles/mapbox/dark-v9', pitch = 45) %>% 
  add_polygon(parcels3, elevation = "tax_per_sqft",
             elevation_scale = 1000000)
## Registered S3 method overwritten by 'jsonify':
##   method     from    
##   print.json jsonlite